﻿using System;
using System.Collections.Concurrent;
using System.Collections.Generic;
using System.Diagnostics;
using System.Linq;
using System.Web.UI;

namespace Morus.Presentation
{
    public class ConventionBasedPresenterDiscoveryStrategy : IPresenterDiscoveryStrategy
    {
        private readonly IBuildManager _buildManager;
        private static readonly ConcurrentDictionary<RuntimeTypeHandle, ConventionSearchResult> _viewTypeToPresenterTypeCache =
            new ConcurrentDictionary<RuntimeTypeHandle, ConventionSearchResult>();
        private static readonly ViewInstanceSuffixCollection _viewInstanceSuffixes = ViewInstanceSuffixes.Suffixes;
        private static readonly CandidatePresenterTypeFullNameFormatCollection _candidatePresenterTypeFullNameFormats = CandidatePresenterTypeFullNameFormats.Formats;

        public ConventionBasedPresenterDiscoveryStrategy(IBuildManager buildManager)
        {
            Check.Argument.IsNotNull(buildManager, "buildManager");

            _buildManager = buildManager;
        }

        public virtual IEnumerable<PresenterDiscoveryResult> GetBindings(IEnumerable<object> hosts, IEnumerable<IView> viewInstances)
        {
            Check.Argument.IsNotNull(hosts, "hosts");
            Check.Argument.IsNotNull(viewInstances, "viewInstances");

            return viewInstances
                .Select(v => GetBinding(v, _buildManager, _viewInstanceSuffixes, _candidatePresenterTypeFullNameFormats))
                .ToArray();
        }

        internal static PresenterDiscoveryResult GetBinding(IView viewInstance, IBuildManager buildManager, IEnumerable<string> viewInstanceSuffixes, IEnumerable<string> presenterTypeFullNameFormats)
        {
            var viewType = viewInstance.GetType();

            var searchResult = _viewTypeToPresenterTypeCache.GetOrAdd(viewType.TypeHandle, (t) =>
                PerformSearch(viewInstance, viewInstanceSuffixes, presenterTypeFullNameFormats, buildManager));

            return new PresenterDiscoveryResult(
                new[] { viewInstance },
                searchResult.Message,
                searchResult.PresenterType == null
                    ? new PresenterBinding[0]
                    : new[] { new PresenterBinding(searchResult.PresenterType, viewType, BindingMode.Default, new[] { viewInstance }) }
            );
        }

        private static ConventionSearchResult PerformSearch(IView viewInstance, IEnumerable<string> viewInstanceSuffixes, IEnumerable<string> presenterTypeFullNameFormats, IBuildManager buildManager)
        {
            var viewType = viewInstance.GetType();
            var presenterType = default(Type);

            // Use the base type for pages & user controls as that is the code-behind file
            // TODO: Ensure using BaseType still works in WebSite projects with code-beside files instead of code-behind files
            if (viewType.Namespace == "ASP" &&
                (typeof(Page).IsAssignableFrom(viewType) || typeof(Control).IsAssignableFrom(viewType)))
            {
                viewType = viewType.BaseType;
            }

            // Get presenter type name from view instance type name
            var presenterTypeNames = new List<string> { GetPresenterTypeNameFromViewTypeName(viewType, viewInstanceSuffixes) };

            // Get presenter type names from implemented IView interfaces
            presenterTypeNames.AddRange(GetPresenterTypeNamesFromViewInterfaceTypeNames(viewType.GetViewInterfaces()));

            // Create candidate presenter type full names
            var candidatePresenterTypeFullNames = GenerateCandidatePresenterTypeFullNames(viewType, presenterTypeNames, presenterTypeFullNameFormats);

            // Ask the build manager to load each type until one is found
            var messages = new List<string>();
            foreach (var typeFullName in candidatePresenterTypeFullNames.Distinct())
            {
                presenterType = buildManager.GetType(typeFullName, false);

                if (presenterType == null)
                {
                    messages.Add("Could not find a presenter with type name {0}. ".FormatWith(typeFullName));
                    continue;
                }

                if (!typeof(IPresenter).IsAssignableFrom(presenterType))
                {
                    messages.Add("Found, but ignored, potential presenter with type name {0} because it does not implement IPresenter. ".FormatWith(typeFullName));
                    presenterType = null;
                    continue;
                }

                messages.Add("Found presenter with type name {0}. ".FormatWith(typeFullName));
                break;
            }

            return new ConventionSearchResult(
                "ConventionBasedPresenterDiscoveryStrategy:\r\n" +
                    string.Join("\r\n", messages.Select(m => "- " + m).ToArray()),
                presenterType
            );
        }

        internal static IEnumerable<string> GetPresenterTypeNamesFromViewInterfaceTypeNames(IEnumerable<Type> viewInterfaces)
        {
            // Trim the "I" and "View" from the start & end respectively of the interface names
            return viewInterfaces
                .Where(i => i.Name != "IView" && i.Name != "IView`1")
                .Select(i => i.Name.TrimStart('I').TrimFromEnd("View"));
        }

        internal static string GetPresenterTypeNameFromViewTypeName(Type viewType, IEnumerable<string> viewInstanceSuffixes)
        {
            // Check for existance of supported suffixes and if found, remove and use result as basis for presenter type name
            // e.g. HelloWorldControl => HelloWorldPresenter
            //      WidgetsWebService => WidgetsPresenter
            var presenterTypeName = (from suffix in viewInstanceSuffixes
                                     where viewType.Name.EndsWith(suffix, StringComparison.OrdinalIgnoreCase)
                                     select viewType.Name.TrimFromEnd(suffix))
                                     .FirstOrDefault();
            return (string.IsNullOrEmpty(presenterTypeName) ? viewType.Name.TrimStart('_') : presenterTypeName) + "Presenter";
        }

        private static IEnumerable<string> GenerateCandidatePresenterTypeFullNames(Type viewType, IEnumerable<string> presenterTypeNames, IEnumerable<string> presenterTypeFullNameFormats)
        {
            // We assume the assembly name is the same as the namespace or that minus ".Web"
            var assemblyName = viewType.Assembly.GetNameSafe();
            var assemblyNameMinusWeb = assemblyName.TrimFromEnd(".Web");

            foreach (var presenterTypeName in presenterTypeNames)
            {
                // Same location as view instance, e.g. MyApp.Web.Controls.HelloWorldControl => MyApp.Web.Controls.HelloWorldPresenter
                yield return viewType.Namespace + "." + presenterTypeName;

                foreach (var typeNameFormat in presenterTypeFullNameFormats)
                {
                    yield return typeNameFormat.Replace("{namespace}", assemblyNameMinusWeb)
                                               .Replace("{presenter}", presenterTypeName);
                }

                if (assemblyName == assemblyNameMinusWeb) continue;

                foreach (var typeNameFormat in presenterTypeFullNameFormats)
                {
                    yield return typeNameFormat.Replace("{namespace}", assemblyName)
                                               .Replace("{presenter}", presenterTypeName);
                }
            }
        }

        internal class ConventionSearchResult
        {
            private readonly string _message;
            private readonly Type _presenterType;

            [DebuggerStepThrough]
            public ConventionSearchResult(string message, Type presenterType)
            {
                _message = message;
                _presenterType = presenterType;
            }

            public string Message
            {
                [DebuggerStepThrough]
                get
                {
                    return _message;
                }
            }

            public Type PresenterType
            {
                [DebuggerStepThrough]
                get
                {
                    return _presenterType;
                }
            }
        }
    }
}